package lumis.portal.sendmail;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.hibernate.exception.LockAcquisitionException;

import lumis.portal.PortalException;
import lumis.portal.UnexpectedException;
import lumis.portal.authentication.SessionConfig;
import lumis.portal.dao.DaoFactory;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.sendmail.dao.ISendMailDao;
import lumis.portal.transaction.PortalTransactionFactory;
import lumis.util.ITransaction;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

/**
 * A implementation of the {@link ISendMailManager} interface.
 * 
 * @version $Revision: 12538 $ $Date: 2011-02-09 12:07:10 -0200 (Wed, 09 Feb 2011) $
 * @since 4.0.4
 */
public class SendMailManager implements ISendMailManager, IMailQueueProcessThreadCallback
{
	/** System logger for this class. */
	private static ILogger logger = LoggerFactory.getLogger(SendMailManager.class);
	
	/** Logger for sending mail information. */
	private static ILogger sendMailLogger = LoggerFactory.getServiceLogger("lumis.service.portalmanagement.sendmail");

	/**
	 * Stores a map of queueId to MailQueueProcessThread. All access to this map must the
	 * synchronized using it as semaphore.
	 * @since 4.0.4
	 */
	private Map<String, MailQueueProcessThread> queueThreads = new HashMap<String, MailQueueProcessThread>();
	
	public IMultipleMail createMultipleMail() throws PortalException
	{
		return DaoFactory.getSendMailDao().createMultipleMail();
	}

	public ISingleMail createSingleMail() throws PortalException
	{
		return DaoFactory.getSendMailDao().createSingleMail();
	}

	public IMailAttachment createMailAttachment() throws PortalException
	{
		return DaoFactory.getSendMailDao().createMailAttachment();
	}

	public IMailDestination createMailDestination() throws PortalException
	{
		return DaoFactory.getSendMailDao().createMailDestination();
	}
	
	public String generateNewQueueId() throws PortalException
	{
		return DaoFactory.getSendMailDao().generateNewQueueId();
	}
		
	public IMailConfig getMailConfig(SessionConfig sessionConfig, ITransaction transaction) throws PortalException
	{
		return DaoFactory.getSendMailDao().getMailConfig(transaction);
	}

	public void setMailConfig(SessionConfig sessionConfig, IMailConfig mailConfig, ITransaction transaction) throws PortalException
	{
		DaoFactory.getSendMailDao().setMailConfig(mailConfig, transaction);
	}

	/** 
	 * Returns a new instance of the {@link IMailSender} to be used by this manager for sending e-mails.
	 * @param sessionConfig user session information.
	 * @param transaction transaction for persistence access.
	 * @return a new mail sender instance.
	 * @since 4.0.4
	 */
	protected IMailSender getMailSender(SessionConfig sessionConfig, ITransaction transaction) throws PortalException
	{
		String mailSenderClass = getMailConfig(sessionConfig, transaction).getMailSenderClass();
		try
		{
			return ManagerFactory.getDeploymentManager().getClassLoader().loadClass(mailSenderClass).asSubclass(IMailSender.class).newInstance();
		}
		catch (Exception e)
		{
			throw new PortalException("STR_COULD_NOT_INSTANTIATE;" + mailSenderClass, e);
		}
	}

	public void sendMail(SessionConfig sessionConfig, ISingleMail mail, ITransaction transaction) throws PortalException
	{
		if (mail == null)
			throw new NullPointerException("mail may not be null");
		
		try
		{
			if (mail.getFrom() == null)
			{
				IMailConfig mailConfig = getMailConfig(sessionConfig, transaction);
				mail.setFrom(mailConfig.getDefaultFromAddress());
			}
			
			IMailSender mailSender = getMailSender(sessionConfig, transaction);
			mailSender.sendMail(sessionConfig, mail, transaction);
			sendMailLogger.info("Mail sent: " + mail.getMailLoggingString());
		}
		catch (Exception e)
		{
			logger.error("Could not send mail: " + mail.getMailLoggingString(), e);
			sendMailLogger.error("Could not send mail: " + mail.getMailLoggingString());
			
			if (e instanceof PortalException)
				throw (PortalException)e;
			else
				throw new UnexpectedException(e);
		}
	}

	public void addMailToSendQueue(SessionConfig sessionConfig, ISingleMail mail, String queueId, ITransaction transaction) throws PortalException
	{
		DaoFactory.getSendMailDao().addMailToSendQueue(mail, queueId, transaction);
	}

	public void addMailToSendQueue(SessionConfig sessionConfig, IMultipleMail mail, String queueId, ITransaction transaction) throws PortalException
	{
		DaoFactory.getSendMailDao().addMailToSendQueue(mail, queueId, transaction);
	}

	/**
	 * Deletes the whole mail if {@link IMailConfig#getRemoveMailsSent()},
	 * otherwise removes only the mail body.
	 * @param mail the mail to be cleaned up.
	 * @since 4.0.4
	 */
	private void cleanUpMailSentOk(IBaseMail mail) throws PortalException
	{
		ITransaction transaction = PortalTransactionFactory.createTransaction();
		try
		{
			transaction.begin();
			IMailConfig mailConfig = getMailConfig(null, transaction);
			if (mailConfig.getRemoveMailsSent())
			{
				DaoFactory.getSendMailDao().deleteMail(mail.getId(), transaction);
			}
			else
			{
				IMailBody body = mail.getBody();
				if (body != null)
					DaoFactory.getSendMailDao().deleteMailBody(mail.getId(), transaction);
			}
			transaction.commit();
		}
		finally
		{
			transaction.dispose();
		}
	}
	
	/**
	 * Sends a queued mail, by correctly updating the mail send status before and
	 * after the mail being sent.
	 * @param mailToSend the mail to be sent.
	 * @param statusToUpdate the mail send status to be updated.
	 * @since 4.0.4
	 */
	private void sendQueuedMail(ISingleMail mailToSend, IMailSendStatus statusToUpdate) throws PortalException
	{
		// mark statusToUpdate as sending
		statusToUpdate.setErrorMessage(null);
		statusToUpdate.setStatus(IMailSendStatus.STATUS_SENDING);
		DaoFactory.getSendMailDao().updateMailSendStatus(statusToUpdate);
		
		try
		{
			// send the mail
			ITransaction transaction = PortalTransactionFactory.createTransaction();
			try
			{
				transaction.begin();
				sendMail(null, mailToSend, transaction);
				transaction.commit();
			}
			finally
			{
				transaction.dispose();
			}
			
			// mark statusToUpdate as sent ok
			statusToUpdate.setSentDateTime(new Date());
			statusToUpdate.setStatus(IMailSendStatus.STATUS_SENT_OK);
			DaoFactory.getSendMailDao().updateMailSendStatus(statusToUpdate);
		}
		catch (Exception e)
		{
			logger.error("Error when trying to send mail", e);

			// mark statusToUpdate as sent error
			statusToUpdate.setStatus(IMailSendStatus.STATUS_SENT_ERROR);
			statusToUpdate.setSentDateTime(new Date());
			statusToUpdate.setErrorMessage(e.getMessage());
			DaoFactory.getSendMailDao().updateMailSendStatus(statusToUpdate);
		}		
	}

	public boolean processNextQueuedMail(String queueId) throws PortalException
	{
		ISendMailDao sendMailDao = DaoFactory.getSendMailDao();
		
		try
		{
			// fetch the mail from the queue
			IBaseMail mail = sendMailDao.fetchNextQueuedMail(queueId);		
			if (mail != null)
			{
				// send the mail
				switch(mail.getMailType())
				{
					case IBaseMail.MAIL_TYPE_MULTIPLE:
						IMultipleMail multipleMail = (IMultipleMail)mail;
						IMailDestination destination = null;
						try
						{
							destination = sendMailDao.fetchNextPendingDestination(multipleMail);
						}
						catch(Exception e)
						{
							logger.warn("Exception fetching next pending email destination: ", e);
							
							// give the multiple mail back to be processed on next iteration
							sendMailDao.unfetchMail(multipleMail.getId());
							break;
						}
	
						if (destination != null)
						{
							try
							{
								// send the mail to the destination
								ISingleMail singleMail = sendMailDao.createSingleMail(multipleMail, destination);
								sendQueuedMail(singleMail, destination);
							}
							catch(Exception e)
							{
								// capture this exception and do not pass on.  This method must return true
								// for the MailQueueProcessThread to continue processing the other destinations.
								logger.warn("Ignoring exception that occurred sending queued mail: ", e);
							}
							finally
							{
								// give the multiple mail back to be processed on next iteration
								sendMailDao.unfetchMail(multipleMail.getId());
							}
						}
						else
						{
							// if all destinations were sent ok, this multiple mail was sent ok.
							// else there were an error in the sending
							int status = IMailSendStatus.STATUS_SENT_OK;
							for (IMailDestination dest: multipleMail.getDestinations())
							{
								if (dest.getStatus() != IMailSendStatus.STATUS_SENT_OK)
								{
									status = IMailSendStatus.STATUS_SENT_ERROR;
									break;
								}
							}
	
							multipleMail.setErrorMessage(null);
							multipleMail.setStatus(status);
							multipleMail.setSentDateTime(new Date());
							sendMailDao.updateMailSendStatus(multipleMail);
							
							if (status == IMailSendStatus.STATUS_SENT_OK)
								cleanUpMailSentOk(multipleMail);
						}					
						break;
						
					case IBaseMail.MAIL_TYPE_SINGLE:
						sendQueuedMail((ISingleMail)mail, mail);
						if (mail.getStatus() == IMailSendStatus.STATUS_SENT_OK)
							cleanUpMailSentOk(mail);
						break;
				}
				
				return true;
			}
			else
				return false;
		}
		catch(LockAcquisitionException e)
		{
			try
			{
				// if received a lock acquisition exception return true to continue trying processing emails
				logger.warn("Ignoring lock acquisition exception that occurred sending queued mail: ", e);
				Thread.sleep(1000);
			}
			catch (InterruptedException interrupt)
			{
				return false;
			}
			return true;
		}
		
	}

	public void threadTerminated(String queueId)
	{
		synchronized (queueThreads)
		{
			queueThreads.remove(queueId);
		}
		try
		{
			DaoFactory.getSendMailDao().unlockQueue(queueId);
		}
		catch (PortalException e)
		{
			logger.error("Error when trying to unlock queue", e);
		}
	}

	public void pollMailQueueProcessing() throws PortalException
	{
		// get the MailConfig
		IMailConfig mailConfig;
		ITransaction transaction = PortalTransactionFactory.createTransaction();
		try
		{
			transaction.begin();
			mailConfig = DaoFactory.getSendMailDao().getMailConfig(transaction);
			transaction.commit();
		}
		finally
		{
			transaction.dispose();
		}
			
		synchronized (queueThreads)
		{
			// while the thread number has not reached maximum number
			while (queueThreads.size() < mailConfig.getMaxSendThreads())
			{
				// if there is another queue to process, create a new thread to process it
				String queueId = DaoFactory.getSendMailDao().lockNextQueue();
				if (queueId != null)
				{
					// create and start a thread to process the queue
					MailQueueProcessThread mailQueueProcessThread = new MailQueueProcessThread(this, queueId, mailConfig.getSendInterval(), mailConfig.getBlockInterval(), mailConfig.getBlockSize());
					queueThreads.put(queueId, mailQueueProcessThread);
					mailQueueProcessThread.start();
				}
				else
				{
					// there is not any queue to process... exit the loop
					break;
				}
			}
		}
	}

	public void deleteMail(SessionConfig sessionConfig, String mailId, ITransaction transaction) throws PortalException
	{
		DaoFactory.getSendMailDao().deleteMail(mailId, transaction);
	}
}
